/*
 * SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

use crate::cli;
use crate::error::ZipSplitError;
use crate::utils;
use crate::zip::{ZipArchive, ZipWriter, END_OF_CENTRAL_DIR_SIZE};
use anyhow::{Ok, Result};
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

pub struct ZipSplitter<'a> {
    archive: ZipArchive,
    args: &'a cli::ZipSplitArgs,
}

impl<'a> ZipSplitter<'a> {
    pub fn new(args: &'a cli::ZipSplitArgs) -> Result<Self> {
        let zip_path = args.zipfile.clone().unwrap();
        if !zip_path.exists() {
            log::error!("Zip file not found: {}", zip_path.display());
            return Err(ZipSplitError::ArchiveNotFound(zip_path.display().to_string()).into());
        }
        let archive = ZipArchive::new(zip_path.to_str().unwrap())?;
        Ok(Self { archive, args })
    }

    pub fn run(&self) -> Result<()> {
        self.check_file_size()?;
        let split_count = self.estimate_split_count();
        let total_size = self.archive.get_total_size();
        let efficiency = Self::calculate_split_efficiency(
            total_size,
            self.args.max_size as u64,
            split_count,
            self.args.room,
        );
        if !self.args.quiet {
            println!(
                "{} zip files would be made ({}% efficiency)",
                split_count, efficiency
            );
        }

        if self.args.test {
            return Ok(());
        }
        self.split_archive(split_count)?;
        Ok(())
    }

    fn check_file_size(&self) -> Result<()> {
        log::debug!("Checking file sizes...");
        let files = self.archive.files().collect::<Vec<_>>();
        for file in &files {
            let file_size = file.data_size() as u32;
            let central_size = file.get_center_header_len();
            if file_size + central_size > self.args.max_size - END_OF_CENTRAL_DIR_SIZE as u32 {
                println!(
                    "zipsplit warning: Entry is larger than max split size of: {}",
                    self.args.max_size - END_OF_CENTRAL_DIR_SIZE as u32
                );
                println!("zipsplit warning: use -n to set split size");
                return Err(ZipSplitError::EntryTooLarge(file.name()).into());
            }
        }
        Ok(())
    }

    fn estimate_split_count(&self) -> u16 {
        let args = self.args;
        let archive = &self.archive;
        if args.sequential {
            let files = archive.files().collect::<Vec<_>>();
            let mut count = 1;
            let mut current_size = 0u32;
            for file in files {
                let file_size = file.data_size() as u32;
                let central_size = file.get_center_header_len();
                let overhead = if current_size == 0 {
                    END_OF_CENTRAL_DIR_SIZE as u32
                } else {
                    0
                };
                let available_size = if count == 1 {
                    args.max_size.saturating_sub(args.room)
                } else {
                    args.max_size
                };
                if current_size + file_size + central_size + overhead > available_size {
                    count += 1;
                    current_size = file_size + central_size + END_OF_CENTRAL_DIR_SIZE as u32;
                } else {
                    current_size += file_size + central_size + overhead;
                }
            }
            count
        } else {
            let files = archive.files().collect::<Vec<_>>();
            let mut used = vec![false; files.len()];
            let mut count = 0;
            while used.iter().any(|&u| !u) {
                let mut batch_total_size = 0u32;
                let available_size = if count == 0 {
                    args.max_size.saturating_sub(args.room)
                } else {
                    args.max_size
                };
                for (i, file) in files.iter().enumerate() {
                    if used[i] {
                        continue;
                    }
                    let file_size = file.data_size() as u32;
                    let central_size = file.get_center_header_len();
                    let overhead = if batch_total_size == 0 {
                        END_OF_CENTRAL_DIR_SIZE as u32
                    } else {
                        0
                    };
                    let next_size = batch_total_size + file_size + central_size + overhead;
                    if next_size > available_size {
                        continue;
                    }
                    batch_total_size = next_size;
                    used[i] = true;
                }
                count += 1;
            }
            count
        }
    }

    fn split_archive(&self, split_count: u16) -> Result<()> {
        let args = self.args;
        let archive = &self.archive;

        // 使用Arc和Mutex包装创建的文件列表
        let created_files = Arc::new(Mutex::new(Vec::<PathBuf>::new()));

        // 创建清理闭包 - 使用Arc克隆
        let cleanup_files = Arc::clone(&created_files);
        let cleanup = move || {
            let files = cleanup_files.lock().unwrap();
            for path in &*files {
                if path.exists() {
                    if let Err(e) = std::fs::remove_file(path) {
                        log::error!("Failed to clean up {}: {}", path.display(), e);
                    } else {
                        log::info!("Cleaned up: {}", path.display());
                    }
                }
            }
        };
        // 设置Ctrl-C处理
        ctrlc::set_handler(move || {
            log::error!("Operation interrupted by user");
            cleanup();
            eprintln!("\nzipsplit error: Interrupted (aborting)");
            std::process::exit(1);
        })
        .map_err(|e| {
            ZipSplitError::InvalidArguments(format!("Failed to set Ctrl-C handler: {}", e))
        })?;

        let mut part_num = 1;
        let mut writer = Self::create_part_writer(part_num, args)?;
        created_files
            .lock()
            .unwrap()
            .push(writer.get_output_path().into());
        let files = archive.files().collect::<Vec<_>>();

        if args.pause {
            print!(
                "Insert disk #{} of {} and hit return: ",
                part_num, split_count
            );
            std::io::stdout().flush()?;
            let mut buf = String::new();
            std::io::stdin().read_line(&mut buf)?;
        }

        let mut used = vec![false; files.len()];
        let mut idx_writer = if args.index {
            let output_path = match &args.temp_path {
                Some(dir) => dir.join("zipsplit.idx"),
                None => "zipsplit.idx".into(),
            };
            log::info!("Creating index file: {}", output_path.display());
            if !args.quiet {
                println!("creating: {}", output_path.display());
            }
            created_files.lock().unwrap().push(output_path.clone());
            Some(File::create(output_path)?)
        } else {
            None
        };
        if !args.quiet {
            println!("creating: {}", writer.get_output_path());
        }

        let mut file_part_map = HashMap::new();

        if args.room > 0 && part_num == 1 {
            log::debug!("Reserved {} bytes in first part", args.room);
        }

        while used.iter().any(|&u| !u) {
            let mut batch_files = Vec::new();
            let mut batch_total_size = 0u32;
            let mut indices = Vec::new();

            if args.sequential {
                let mut current_index = 0;
                while current_index < files.len() {
                    // 跳过已处理的文件
                    if used[current_index] {
                        current_index += 1;
                        continue;
                    }

                    let file = &files[current_index];
                    let file_size = file.data_size() as u32;
                    let central_size = file.get_center_header_len();
                    let overhead = if batch_files.is_empty() {
                        END_OF_CENTRAL_DIR_SIZE as u32
                    } else {
                        0
                    };
                    let available_size = if part_num == 1 {
                        args.max_size.saturating_sub(args.room)
                    } else {
                        args.max_size
                    };

                    if file_size + central_size + overhead > available_size {
                        log::debug!(
                            "File '{}' size {} exceeds max size {}",
                            file.name(),
                            file_size,
                            available_size
                        );
                        current_index += 1;
                        continue;
                    }

                    if batch_total_size + file_size + central_size + overhead <= available_size {
                        batch_files.push(file);
                        batch_total_size += file_size + central_size + overhead;
                        indices.push(current_index);
                        file_part_map.insert(current_index, part_num);
                        current_index += 1;
                    } else {
                        break;
                    }
                }
            } else {
                for (i, file) in files.iter().enumerate() {
                    if used[i] {
                        continue;
                    }
                    let file_size = file.data_size() as u32;
                    let central_size = file.get_center_header_len();
                    let overhead = if batch_files.is_empty() {
                        END_OF_CENTRAL_DIR_SIZE as u32
                    } else {
                        0
                    };
                    let available_size = if part_num == 1 {
                        args.max_size.saturating_sub(args.room)
                    } else {
                        args.max_size
                    };
                    let next_size = batch_total_size + file_size + central_size + overhead;
                    if next_size > available_size {
                        continue;
                    }
                    batch_files.push(file);
                    batch_total_size = next_size;
                    indices.push(i);
                    file_part_map.insert(i, part_num);
                }
            }

            if batch_files.is_empty() {
                let (_, file) = files.iter().enumerate().find(|(i, _)| !used[*i]).unwrap();
                let file_size = file.data_size();

                log::debug!(
                    "batch_total_size: {}, file_size: {}, central_size: {}",
                    batch_total_size,
                    file_size,
                    file.get_center_header_len()
                );
                return Err(ZipSplitError::EntryTooLarge(format!(
                    "File '{}' size {} exceeds max size {}",
                    file.name(),
                    file_size,
                    args.max_size
                ))
                .into());
            }

            log::debug!(
                "Writing {} files to part {}, total size {}",
                batch_files.len(),
                part_num,
                batch_total_size
            );
            for file in batch_files {
                writer.raw_copy_file(file, file.name())?;
            }
            for &i in &indices {
                used[i] = true;
            }

            writer.finish()?;
            part_num += 1;
            if used.iter().any(|&u| !u) {
                writer = Self::create_part_writer(part_num, args)?;
                created_files
                    .lock()
                    .unwrap()
                    .push(writer.get_output_path().into());
                if args.pause {
                    print!(
                        "Insert disk #{} of {} and hit return: ",
                        part_num, split_count
                    );
                    std::io::stdout().flush()?;
                    let mut buf = String::new();
                    std::io::stdin().read_line(&mut buf)?;
                }
                if !args.quiet {
                    println!("creating: {}", writer.get_output_path());
                }
            }
        }

        if let Some(ref mut idx) = idx_writer {
            for (i, file) in files.iter().enumerate() {
                if let Some(part) = file_part_map.get(&i) {
                    writeln!(idx, "{} {}", part, file.name())?;
                }
            }
        }

        Ok(())
    }

    fn create_part_writer(part_num: usize, args: &cli::ZipSplitArgs) -> Result<ZipWriter> {
        let zipfile = args.zipfile.as_ref().unwrap();
        let base_name = zipfile
            .file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("zipsplit");
        let part_name = format!("{}{}.zip", base_name, part_num);
        let output_path = match &args.temp_path {
            Some(dir) => dir.join(part_name),
            None => part_name.into(),
        };
        let writer = ZipWriter::new(output_path.to_str().unwrap())?;
        Ok(writer)
    }

    fn calculate_split_efficiency(
        total_size: u64,
        max_size: u64,
        actual_splits: u16,
        _room: u32,
    ) -> u8 {
        let theoretical_min = ((total_size + max_size - 1) / max_size) as u16;
        let efficiency = (200 * theoretical_min as u32 / actual_splits as u32) as u16;
        ((efficiency + 1) / 2).min(100) as u8
    }
}

pub fn main() -> Result<()> {
    utils::common::init_logger();
    let args = cli::parse_args_split();

    log::debug!("Parsed arguments: {:?}", args);

    if args.version {
        cli::show_version_split();
        return Ok(());
    }
    if args.license {
        cli::show_license_split();
        return Ok(());
    }
    if args.zipfile.is_none() {
        log::error!("Zip file not specified!");
        return Err(ZipSplitError::InvalidArguments("need to specify zip file".to_string()).into());
    }

    let splitter = ZipSplitter::new(&args)?;
    splitter.run()
}
